-- ************************************************************************** --
-- * TitanWG.lua                                                            * --
-- *                                                                        * --
-- *  By: Rothpada                                                          * --
-- *  Thanks to: Azime                                                      * --
-- *  Translations by: Gomntep (deDE), LigthSora (esES,esMX),               * -- 
-- *                   StingerSoft (ruRU), Alain (frFR)                     * --
-- ************************************************************************** --

local TITAN_WG_ID = "TitanWG";
local AceTimer = LibStub("AceTimer-3.0");
local L = LibStub("AceLocale-3.0"):GetLocale("TitanWG", true)

		---------------
		-- CONSTANTS --
		---------------	

local TitanWG_SYNC_REGEX = "(%w+) (.*) (.*)"; 		-- Regex for sync messages
local TitanWG_NORTHREND_INDEX = 4; 					-- the continent number
local TitanWG_WINTERGRASP_INDEX = 10; 				-- the zone number 
local TitanWG_LEVEL_THRESHOLD = 80; 				-- For tracking VoA and quest information
local TitanWG_MAX_BATTLE_LENGTH = 30*60; 			-- 30 minutes, in seconds
local TitanWG_MAX_TIME_BETWEEN_BATTLES = 2.5*3600;	-- 2.5 hours, in seconds
local TitanWG_WARNING_TIME = 10*60; 				-- 10 minute warning, in seconds
local TitanWG_EXPIRED_SYNC_THRESHOLD = 30*86400; 	-- 30 days, in seconds
local TitanWG_REQUEST_TIME_S = 3; 					-- How long to wait before processing the replies in the pool

local TitanWG_IS_DEBUGGING = ( false
								and ( GetRealmName() == "Shadow Council" 
										or GetRealmName() == "Garithos" ))
								or UnitName("player") == "Balther";

	-- "Enumeration" to denote the possible addon states --
	
local TitanWG_STATE_CURRENTLY_IN_NORTHREND 		= 0;	-- Denotes the character is currently in Northrend and there is not a battle underway 
local TitanWG_STATE_RECENTLY_IN_NORTHREND 		= 1;	-- The character has been in Northrend recently and there has not been a battle since 
local TitanWG_STATE_BATTLE_UNDERWAY 			= 2;	-- The character is currently in Northrend and there is currently a battle underway in Northrend
local TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND	= 3;	-- The character has been in Northrend this session but not since the last battle
local TitanWG_STATE_NOT_BEEN_TO_NORTHREND 		= 4;	-- The character has not been to Northrend this session nor received a sync

		---------------
		-- VARIABLES --
		---------------

local TitanWG_variablesLoaded = false;		-- True iff the variables have been loaded
local TitanWG_announced = false;			-- Whether we already auto-announced
local TitanWG_continent = -1;				-- The current continent
local TitanWG_owner = nil;					-- The faction that owns WG currently, may be nil
local TitanWG_player = nil;					-- The player's name; should only be nil before the player has entered the world
local TitanWG_realm = nil;					-- The realm's name; should only be nil before the player has entered the world
local TitanWG_timePool = {};				-- A gathering place for sync time replies
local TitanWG_isProcessingPool = false;		-- True iff there is a pool currently being processed; used to guard the pool against contamination
local TitanWG_isRequestPending = false;		-- True iff there is a request sent and not processed; used to guard against multiple requests

		-------------
		-- STRUCTS --
		-------------

	-- The state machine's struct --
	
local TitanWG_State = {};
	-- The current state we're in
	TitanWG_State.state = TitanWG_STATE_NOT_BEEN_TO_NORTHREND;
	-- Returns the amount of time remaining until the next battle starts.  The
	-- actual function depends upon the state
	TitanWG_State.time = function() return TitanWG_getTimeEstimate(); end
	-- Returns true if the time information we have is accurate, where
	-- accurate means that either we are, or have recently been, in NR or
	-- we have received a sync within the last 15m and the estimated error
	-- is not greater than the amount of time we think is left.
	TitanWG_State.isInfoAccurate = function() 
		local stale = true;
		local wrong = true;
		if ( TimeOfLastTimes ~= nil and TimeOfLastTimes[TitanWG_realm] ~= nil ) then
			stale = time() - TimeOfLastTimes[TitanWG_realm] > 900;
		end
		local stateTime = TitanWG_State.time();
		if ( stateTime ~= nil ) then
			wrong = TitanWG_State.time() > 2*TitanWG_getEstimatedError();
		end
		return TitanWG_State.state <= TitanWG_STATE_RECENTLY_IN_NORTHREND
			and TitanWG_State.state ~= TitanWG_STATE_BATTLE_UNDERWAY  -- Fix for issue reported by hohner 
			or ( ( not stale ) and ( not wrong ) ); 
	end
	-- The owner function.  This returns who the current owner of WG is, or 
	-- nil if we have no current information
	TitanWG_State.updateOwner = function() 
		if ( TitanWG_owner == nil --and TitanWG_State.isInfoAccurate() 
				and TitanWG_isInNorthrend() ) then
			-- See if we have the WG buff
			
			-- Otherwise, use map landmarks
			local continentIndex = GetCurrentMapContinent();
			local zoneIndex = GetCurrentMapZone();
			-- set the map to Northrend
			SetMapZoom(TitanWG_NORTHREND_INDEX);
			-- Get the information we need from it
			local name, description, textureIndex, x, y;
			for i=1,GetNumMapLandmarks() do
				name,_,textureIndex,_,_ = GetMapLandmarkInfo(i);
				if ( name == L["TITAN_WG_WINTERGRASP"] ) then
					if ( textureIndex == 48 ) then 
						TitanWG_owner = L["TITAN_WG_HORDE"];
						break;
					elseif ( textureIndex == 46 ) then
						TitanWG_owner = L["TITAN_WG_ALLIANCE"];
						break;
					elseif ( textureIndex == 101 ) then
						break;
					else
						TitanWG_debug("textureIndex="..textureIndex);
					end
				end
			end
			-- set the map back
			SetMapZoom(continentIndex, zoneIndex, 1);
			if ( TitanWG_owner == nil ) then
				TitanWG_debug("Current owner: nil");
			else
				TitanWG_debug("Current owner: "..TitanWG_owner);
			end
		end
	end
	-- This function returns a string containing information about WG
	TitanWG_State.description = function()
		local timeStr = TitanWG_getColouredText(
					TitanWG_formatTime(TitanWG_State.time()), 
					"white"
				);
		if ( timeStr == nil ) then
			timeStr = "---";
		end
		if TitanWG_State.isInfoAccurate() then 
			-- If we have accurate info, we're happy
			return L["TITAN_WG_CONTROLLER"]..":\t"..TitanWG_colourFactionText(TitanWG_owner).."\n"
				..L["TITAN_WG_NEXT_BATTLE"]..":\t"..timeStr.."\n\n";
		else
			if TitanWG_State.state == TitanWG_STATE_BATTLE_UNDERWAY then
				-- If the battle's underway then we want to show 
				-- how much time has elapsed since it started
				return TitanWG_colourFactionText(L["TITAN_WG_BATTLE_UNDERWAY"]).."\n"
					..L["TITAN_WG_TIME_ELAPSED"]..":\t"..timeStr.."\n\n";
			else
				-- We don't have accurate information
				local dt = time() - TimeOfLastTimes[TitanWG_realm];
				local dtStr = TitanWG_getColouredText(TitanWG_formatTime(dt), "white");
				local eps = TitanWG_getEstimatedError();
				local epsStr = TitanWG_getColouredText("\194\177"..TitanWG_formatTime(eps), "white");
				local wgTime = TitanWG_State.time();
				
				if ( 2*eps < wgTime ) then
					timeStr = TitanWG_getColouredText(
						TitanWG_formatTime(wgTime - eps), 
						"white"
					);
					return TitanWG_getColouredText(L["TITAN_WG_NO_ZONE_INFO"], "yellow").."\n\n"
						..L["TITAN_WG_NEXT_BATTLE"]..":\t~"..timeStr.."\n"
						..L["TITAN_WG_TIME_OF_LAST_SYNC"]..":\t"..dtStr.."\n"
						..L["TITAN_WG_PROBABLE_ERROR"]..":\t"..epsStr.."\n\n";
				else
					TitanWG_debug("Estimated error is outside the tolerance.");
					return TitanWG_getColouredText(L["TITAN_WG_NO_ZONE_INFO"], "yellow").."\n\n";
				end
			end
		end 
	end
	TitanWG_State.buttonText = function() 
		local wgTime = TitanWG_State.time();
	
		if ( wgTime ~= nil ) then
			if ( TitanWG_State.state == TitanWG_STATE_CURRENTLY_IN_NORTHREND ) then
				-- This is the time reported by Blizzard's API function
				return TitanWG_colourFactionText(TitanWG_formatTime(wgTime));
			elseif ( TitanWG_State.state == TitanWG_STATE_RECENTLY_IN_NORTHREND ) then
				-- This is an estimate, though it should be quite accurate.  Also,
				-- our ownership information is still accurate
				return TitanWG_colourFactionText("~"..TitanWG_formatTime(wgTime));
			elseif ( TitanWG_State.state == TitanWG_STATE_BATTLE_UNDERWAY ) then
				-- Give the time elapsed since the battle started
				return TitanWG_getColouredText("~"..TitanWG_formatTime(wgTime), "yellow");
			elseif ( TitanWG_State.state >= TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND ) then
				-- We provide our best estimate
				return TitanWG_getColouredText("<"..TitanWG_formatTime(wgTime), "white");				
			end
		end
		return "---";
	end

-- ************************************************************************** --
-- ******************************** Titan Functions ************************* --
-- ************************************************************************** --

function TitanPanelTitanWGButton_OnLoad(self)
	self.registry = {
		id = TITAN_WG_ID,
		menuText = L["TITAN_WG_MENU_TEXT"],
		buttonTextFunction = "TitanPanelTitanWGButton_GetButtonText",
		tooltipTitle = L["TITAN_WG_TOOLTIP_TITLE"],
		tooltipTextFunction = "TitanPanelTitanWGButton_GetTooltipText",
		savedVariables = {
			ShowLabelText = 1,
			EnableGuildAnnouncing = false,
			EnableTradeAnnouncing = false,
			DefaultChannel = "GUILD",
		}
	};	
	
	TitanWG_debug("Debugging output is turned on");
	
	self:RegisterEvent("PLAYER_ENTERING_WORLD");	
	self:RegisterEvent("VARIABLES_LOADED");		 	
	self:RegisterEvent("CHAT_MSG_ADDON");		 
	-- Schedule a timer to update the button
	AceTimer.ScheduleRepeatingTimer("TitanPanel"..TITAN_WG_ID, 
		TitanPanelPluginHandle_OnUpdate, 1, {TITAN_WG_ID, TITAN_PANEL_UPDATE_ALL });
	-- Schedule a timer to request a sync every 5 minutes
	AceTimer.ScheduleRepeatingTimer("TitanWG_syncTimer", TitanWG_requestWGSync, 300);
	
	DEFAULT_CHAT_FRAME:AddMessage(TITAN_WG_ID.." by Rothpada (Shadow Council)", 1, 1, 1)
end 

function TitanPanelTitanWGButton_OnEvent(self, event, ...)
	if ( event == "PLAYER_ENTERING_WORLD" ) then
		TitanWG_continent = GetCurrentMapContinent();
		TitanWG_player,_ = UnitName("player");
		TitanWG_realm = GetRealmName();
		if ( TitanWG_isInNorthrend() ) then
			TitanWG_owner = nil; -- Fix for bug 19: causes a rediscover on the owner
		else
			TitanWG_requestWGSync();
		end
		TitanWG_updateState();
		TitanWG_debug("Player has entered the world.");
	elseif ( event == "VARIABLES_LOADED" ) then
		-- Initialize time variables
		if ( not TimeOfLastTimes ) then 
			TimeOfLastTimes = {}; 
		end
		if ( not TimeOfLastTimes[GetRealmName()] ) then 
			TimeOfLastTimes[GetRealmName()] = -1; 
		end
		if ( not LastTimesSeen ) then 
			LastTimesSeen = {}; 
		end
		if ( not LastTimesSeen[GetRealmName()] ) then 
			LastTimesSeen[GetRealmName()] = -1; 
		end
		-- Initialize vault tables
		if ( not VoAData ) then 
			VoAData = {}; 
		end
		if ( UnitLevel("player") >= TitanWG_LEVEL_THRESHOLD and 
						  ( not VoAData[GetRealmName()] ) ) then 
			VoAData[GetRealmName()] = {}; 
		end
		if ( UnitLevel("player") >= TitanWG_LEVEL_THRESHOLD and 
				( not VoAData[GetRealmName()][UnitName("player")] ) ) then 
			VoAData[GetRealmName()][UnitName("player")] = {}; 
		end
		-- Initialize quest tables
		--[[ if ( not QuestData ) then 
			QuestData = {}; 
		end
		if ( UnitLevel("player") >= TitanWG_LEVEL_THRESHOLD and 
				( not QuestData[GetRealmName()] ) ) then 
			QuestData[GetRealmName()] = {}; 
		end
		if ( UnitLevel("player") >= TitanWG_LEVEL_THRESHOLD and 
				( not QuestData[GetRealmName()][UnitName("player")] ) ) then 
			QuestData[GetRealmName()][UnitName("player")] = {}; 
		end ]]
		-- Initialize player's network 
		if ( not SyncNetData ) then 
			SyncNetData = {}; 
		end
		if ( not SyncNetData[GetRealmName()] ) then 
			SyncNetData[GetRealmName()] = {}; 
		end
		-- Initialize the error estimating tables
		if ( not EPS ) then
			EPS = {};
		end
		if ( not EPS[GetRealmName()] ) then
			EPS[GetRealmName()] = 0;
		end
		if ( not T ) then
			T = {};
		end
		if ( not T[GetRealmName()] ) then
			T[GetRealmName()] = 0;
		end
		TitanWG_variablesLoaded = true;
		TitanWG_debug("Variables are loaded.");
	elseif ( event == "CHAT_MSG_ADDON" ) then
		local prefix, message, channel, sender = select(1,...);
		if ( prefix == TITAN_WG_ID and sender ~= UnitName("player") ) then
			TitanWG_debug("Message: "..message);
			if ( message == "REQUEST" ) then
				TitanWG_sendWGSync(channel, sender);
			else
				local cmd, wgTime, wgOwner = strmatch(message, TitanWG_SYNC_REGEX);
				if ( cmd == "UPDATE" ) then
					TitanWG_processWGSync(sender, wgTime, wgOwner);
				end
			end
		end
	end
end

function TitanPanelTitanWGButton_OnClick(self, button)
	if ( button == "LeftButton" ) then
		TitanWG_postTimeToChannel(TitanGetVar(TITAN_WG_ID, "DefaultChannel"));
	end
end

function TitanPanelTitanWGButton_GetButtonText(id)
	TitanWG_updateState();
		
	-- Check the auto-announce, if enabled and in a guild and our time is
	-- going to be reasonably accurate
	local wgTime = TitanWG_State.time();
	if ( wgTime ~= nil ) then
		if ( TitanWG_State.isInfoAccurate() ) then
			if ( wgTime == TitanWG_WARNING_TIME and ( not TitanWG_announced ) ) then
				if ( IsInGuild() and TitanGetVar(TITAN_WG_ID, "EnableGuildAnnouncing") ) then 
					TitanWG_postTimeToChannel("GUILD");
				end
				local channel = TitanWG_getTradeChannel();
				if ( TitanGetVar(TITAN_WG_ID, "EnableTradeAnnouncing") and channel ~= nil ) then -- Fix for bug 18
					TitanWG_postTimeToChannel("CHANNEL", channel);
				end
				TitanWG_announced = true; -- Makes sure this only triggers once
			end
		end
		if ( wgTime < TitanWG_WARNING_TIME ) then
			TitanWG_announced = false;
		end
	end

	return L["TITAN_WG_BUTTON_LABEL"], TitanWG_State.buttonText();
end

function TitanPanelTitanWGButton_GetTooltipText(self)
	local tooltip = "\n"..TitanWG_State.description();

	-- Show number of WG marks and shards
	
	tooltip = tooltip..TitanWG_getColouredText(L["TITAN_WG_WINTERGRASP_CURRENCY_INFO"], "white")..":\n";
	local noCurrencies = true;
	for i=1,GetCurrencyListSize() do
		name, _, _, _, _, count, _, _, itemID = GetCurrencyListInfo(i);
		if ( itemID == 43228 or itemID == 43589 ) then
			noCurrencies = false;
			tooltip = tooltip..format("     %s:\t%s\n", TitanWG_getColouredText(name, "yellow"), TitanWG_getColouredText(count, "white"));
		end
	end              
	
	if noCurrencies then
		tooltip = tooltip.."     "..TitanWG_getColouredText(L["TITAN_WG_NO_CURRENCIES"], "red").."\n";
	end
	
	-- Show vault data

	if ( UnitLevel("player") >= TitanWG_LEVEL_THRESHOLD ) then
		-- Show if character is saved to Vault 10 or 25		
		VoAData[TitanWG_realm][TitanWG_player]["normal"] = nil;
		VoAData[TitanWG_realm][TitanWG_player]["heroic"] = nil;
	
		for i=1,GetNumSavedInstances() do
			local name, ID, expiry, difficulty = GetSavedInstanceInfo(i);
			expiry = time() + expiry;
			if ( name == L["TITAN_WG_VAULT_OF_ARCHAVON"] ) then
				if ( difficulty > 1 ) then
					VoAData[TitanWG_realm][TitanWG_player]["heroic"] = expiry;
				else
					VoAData[TitanWG_realm][TitanWG_player]["normal"] = expiry;
				end
			end
		end
	end
	
	tooltip = tooltip.."\n"..TitanWG_getColouredText(L["TITAN_WG_VAULT_OF_ARCHAVON_INFO"], "white")..":\n";
	
	for realm,players in pairs(VoAData) do
		tooltip = tooltip.."   "..TitanWG_getColouredText(realm, "daa520").."\n";
		for player,raids in pairs(VoAData[realm]) do
			if ( VoAData[realm][player]["normal"] ~= nil and time() > VoAData[realm][player]["normal"] ) then
				VoAData[realm][player]["normal"] = nil;
			end
			if ( VoAData[realm][player]["heroic"] ~= nil and time() > VoAData[realm][player]["heroic"] ) then
				VoAData[realm][player]["heroic"] = nil;
			end
			
			local v10t = "";
			local v25t = "";
			if ( VoAData[realm][player]["normal"] == nil ) then
				v10t = TitanWG_getColouredText(L["TITAN_WG_UNSAVED"], "green");
			else
				v10t = TitanWG_getColouredText(L["TITAN_WG_SAVED"], "red");
			end
			if ( VoAData[realm][player]["heroic"] == nil ) then
				v25t = TitanWG_getColouredText(L["TITAN_WG_UNSAVED"], "green");
			else
				v25t = TitanWG_getColouredText(L["TITAN_WG_SAVED"], "red");
			end
			-- TODO: Colour the current player differently
			if ( player == UnitName("player") ) then
				tooltip = tooltip..format("      %s - \t10: %s 25: %s\n", 
										TitanWG_getColouredText(player, "ffa500"),
										v10t, v25t
									 );
			else
				tooltip = tooltip..format("      %s - \t10: %s 25: %s\n", 
										TitanWG_getColouredText(player, "yellow"),
										v10t, v25t
									 );
			end	
		end
	end
	
    return tooltip;
end

function TitanPanelRightClickMenu_PrepareTitanWGMenu()
	local info;
	if ( UIDROPDOWNMENU_MENU_LEVEL == 2 ) then
		if ( UIDROPDOWNMENU_MENU_VALUE == "TITAN_WG_BROADCAST" ) then
			info = {};
			info.text = L["TITAN_WG_SAY"];
			info.func = function() TitanWG_postTimeToChannel("SAY"); end
			UIDropDownMenu_AddButton(info,2);
			info = {};
			info.text = L["TITAN_WG_YELL"];
			info.func = function() TitanWG_postTimeToChannel("YELL"); end
			UIDropDownMenu_AddButton(info,2);
			info = {};
			info.text = L["TITAN_WG_GUILD"];
			info.func = function() TitanWG_postTimeToChannel("GUILD"); end
			UIDropDownMenu_AddButton(info,2);
			info = {};
			info.text = L["TITAN_WG_PARTY"];
			info.func = function() TitanWG_postTimeToChannel("PARTY"); end
			UIDropDownMenu_AddButton(info,2);
			info = {};
			info.text = L["TITAN_WG_RAID"];
			info.func = function() TitanWG_postTimeToChannel("RAID"); end
			UIDropDownMenu_AddButton(info,2);
			local list = {GetChannelList()};
			for i=1,getn(list)/2 do
				info = {};
				info.text = list[i*2-1]..". "..list[i*2];
				info.func = function() TitanWG_postTimeToChannel("CHANNEL",list[i*2-1]); end
				UIDropDownMenu_AddButton(info,2);
			end
			
			TitanPanelRightClickMenu_AddSpacer(2);
			
			info = {};
			info.text = L["TITAN_WG_WHISPER"];
			info.func = function()
				StaticPopupDialogs["TITAN_WG_WHISPER_DIALOG"] = {
				  text = L["TITAN_WG_WHISPER_TEXT"],
				  button1 = L["TITAN_WG_ACCEPT"],
				  button2 = L["TITAN_WG_CANCEL"],
				  OnAccept = function() 
				  	TitanWG_postTimeToChannel("WHISPER", getglobal(this:GetParent():GetName().."EditBox"):GetText());
				  end,
				  timeout = 0,
				  whileDead = 1,
				  hideOnEscape = 1,
				  hasEditBox = 1
				};
				StaticPopup_Show("TITAN_WG_WHISPER_DIALOG");
			end
			UIDropDownMenu_AddButton(info,2);
		end
	else
		TitanPanelRightClickMenu_AddTitle(TitanPlugins[TITAN_WG_ID].menuText);
		TitanPanelRightClickMenu_AddTitle(L["TITAN_WG_CHANNEL"], 2); 
		TitanPanelRightClickMenu_AddSpacer();
		
		info = {};
		info.text = L["TITAN_WG_BROADCAST"];
		info.value = "TITAN_WG_BROADCAST";
		info.hasArrow = 1;
		UIDropDownMenu_AddButton(info);
		if IsInGuild() then
			TitanPanelRightClickMenu_AddToggleVar(L["TITAN_WG_AUTO_ANNOUNCE_GUILD"], TITAN_WG_ID, "EnableGuildAnnouncing",2);
		end
		TitanPanelRightClickMenu_AddToggleVar(L["TITAN_WG_AUTO_ANNOUNCE_TRADE"], TITAN_WG_ID, "EnableTradeAnnouncing",2);
				
		TitanPanelRightClickMenu_AddSpacer();
		TitanPanelRightClickMenu_AddToggleLabelText(TITAN_WG_ID);
		TitanPanelRightClickMenu_AddSpacer();
		TitanPanelRightClickMenu_AddCommand(L["TITAN_WG_HIDE"], TITAN_WG_ID, TITAN_PANEL_MENU_FUNC_HIDE);
	end
end

-- ************************************************************************** --
-- ******************************** TitanWG Functions *********************** --
-- ************************************************************************** --

-- Colours the input text according to which faction controls Wintergrasp.
-- That is, if we don't have accurate information then the colour is white.
-- If we do, then the colour is green is the player's faction owns WG, red if
-- not, or yellow if there is a battle underway.
function TitanWG_colourFactionText(msg)
	local colour = "white";
	if ( TitanWG_State.isInfoAccurate() ) then
		if ( TitanWG_State.time() < 0 ) then
			colour = "yellow";
		elseif ( TitanWG_owner ~= nil ) then
			if ( TitanWG_owner == UnitFactionGroup("player") ) then
				colour = "green";
			else  
				colour = "red";
			end
		end
	end
	
	return TitanWG_getColouredText(msg, colour);
end

-- Returns true iff the player is in Northrend and not in an instance
function TitanWG_isInNorthrend()
	return TitanWG_continent == TitanWG_NORTHREND_INDEX and not IsInInstance();
end

-- Updates the state struct
function TitanWG_updateState()
	local oldState = TitanWG_State.state;
	local oldTime = 0;
	-- Update the state that we're in
	if TitanWG_isInNorthrend() then
		if GetWintergraspWaitTime() then
			if ( oldState >= TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND ) then
				oldTime = TitanWG_State.time();
			end
			TitanWG_State.state = TitanWG_STATE_CURRENTLY_IN_NORTHREND;
		else
			TitanWG_owner = nil;
			TitanWG_State.state = TitanWG_STATE_BATTLE_UNDERWAY;
		end
	elseif ( TitanWG_State.state == TitanWG_STATE_BATTLE_UNDERWAY ) then
		-- We consider our information stale because we don't know how long
		-- the battle will take, only an upper bound on that time
		TitanWG_owner = nil;
		TitanWG_State.state = TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND;
	elseif ( TitanWG_State.state == TitanWG_STATE_CURRENTLY_IN_NORTHREND ) then
		-- We only left NR recently so our information is still accurate
		TitanWG_State.state = TitanWG_STATE_RECENTLY_IN_NORTHREND;
	elseif ( TitanWG_State.state == TitanWG_STATE_RECENTLY_IN_NORTHREND ) then
		-- We're not in NR.  See if we estimate that a battle has started and 
		-- must have ended
		if ( TitanWG_State.time() <= -TitanWG_MAX_BATTLE_LENGTH ) then
			-- If so, we say we've not recently been in NR and can provide at
			-- best an upper bound
			TitanWG_owner = nil;
			TitanWG_State.state = TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND;
		end
	end
	
	if ( TitanWG_owner == nil and TitanWG_State.state <= TitanWG_STATE_RECENTLY_IN_NORTHREND ) then
		-- Update the WG owner
		TitanWG_State.updateOwner();
	end
	
	if ( oldState ~= TitanWG_State.state ) then
		TitanWG_debug("old state: "..oldState..",new state: "..TitanWG_State.state);
		
		-- Create the time function to find out how much time is left
		if ( TitanWG_State.state >= TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND ) then
			-- Get an estimate on the time left from our best available source
			TitanWG_State.time = function() return TitanWG_getTimeEstimate(); end
		elseif ( TitanWG_State.state == TitanWG_STATE_RECENTLY_IN_NORTHREND ) then 
			-- We're not in NR but have good info so we calculate how much time we have left
			TitanWG_State.time = function() 
				return LastTimesSeen[TitanWG_realm] - (time() - TimeOfLastTimes[TitanWG_realm]); 
			end
		else 
			-- We are in NR so GetWintergraspWaitTime will work
			if ( TitanWG_State.state == TitanWG_STATE_CURRENTLY_IN_NORTHREND ) then 
				-- We are between battles
				 TitanWG_State.time = function()
					LastTimesSeen[TitanWG_realm] = GetWintergraspWaitTime();
					TimeOfLastTimes[TitanWG_realm] = time();
					return LastTimesSeen[TitanWG_realm];
				end
			else 
				-- the battle is underway, so return how long the battle's been going for
				TitanWG_State.time = function() 
					return abs(LastTimesSeen[TitanWG_realm] - (time() - TimeOfLastTimes[TitanWG_realm])); 
				end
			end
		end
	end
									
	if ( oldState >= TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND and 
			TitanWG_State.state == TitanWG_STATE_CURRENTLY_IN_NORTHREND ) then
		if ( TimeOfLastTimes[TitanWG_realm] > 0 and oldTime > 0 ) then
			local dt = time() - TimeOfLastTimes[TitanWG_realm];
			local eps = abs(oldTime - TitanWG_State.time());
			TitanWG_learnEstimatedError(dt, eps);
		end
	end
	
end

function TitanWG_debug(msg)
	if ( msg == nil ) then
		msg = "<nil>";
	end
	if TitanWG_IS_DEBUGGING then
		TitanDebug("TitanWG> "..msg);
	end
end

function TitanWG_formatTime(timeInSeconds)
	return TitanUtils_GetAbbrTimeText(timeInSeconds);
end

function TitanWG_getColouredText(msg, colour)
	local colourCode = "";
	
	if ( msg == nil ) then
		return "";
	end
		
	if ( colour == nil or colour == "default" ) then
		return msg;
	elseif ( colour == "red" ) then
		colourCode = "|cffff0000";
	elseif ( colour == "green" ) then
		colourCode = "|cff00ff00";
	elseif ( colour == "blue" ) then
		colourCode = "|cff0000ff";
	elseif ( colour == "yellow" ) then
		colourCode = "|cffffff00";
	elseif ( colour == "white" ) then
		colourCode = "|cffffffff";
	elseif ( colour == "black" ) then
		colourCode = "|cff000000";
	else
		colourCode = "|cff"..colour;
	end
        
	return colourCode..msg..FONT_COLOR_CODE_CLOSE;
end

function TitanWG_getTimeEstimate()
	if ( TimeOfLastTimes[TitanWG_realm] == nil ) then
		return nil;
	end
	-- We provide an upper bound if we've not recently been to Northrend
	local bound;
	local tmax = TitanWG_MAX_BATTLE_LENGTH + TitanWG_MAX_TIME_BETWEEN_BATTLES;
	local tn = time();
	local t0 = TimeOfLastTimes[TitanWG_realm] + LastTimesSeen[TitanWG_realm];
	
	if ( t0 < tn ) then
		bound = tmax - mod(tn - t0, tmax); 	
	else
		bound = t0 - tn;
	end
	
	return bound;
end

function TitanWG_getEstimatedError(numSec)
	if ( numSec == nil ) then
		numSec = time() - TimeOfLastTimes[TitanWG_realm];
	end
	return ceil(numSec * EPS[TitanWG_realm] / T[TitanWG_realm]);
end

function TitanWG_learnEstimatedError(dt, eps)
	local oldError = TitanWG_getEstimatedError(dt);
	local oldEPS = EPS[TitanWG_realm]/T[TitanWG_realm];
	T[TitanWG_realm] = T[TitanWG_realm] + dt;
	EPS[TitanWG_realm] = EPS[TitanWG_realm] + abs(eps);
	TitanWG_debug("Learning error: dt="..dt..",eps="..eps
					..",Expected error: "..oldError
					..",Old EPS: "..oldEPS
					..",New EPS: "..(EPS[TitanWG_realm]/T[TitanWG_realm]));
end

function TitanWG_postTimeToChannel(channel,target)
	local wgTime = TitanWG_State.time();
	if ( channel ~= nil and wgTime ~= nil ) then
		if ( TitanWG_State.isInfoAccurate ) then 
			SendChatMessage("<"..TITAN_WG_ID.."> "..L["TITAN_WG_TIME_UNTIL_WG"]..": "..TitanWG_formatTime(wgTime),channel,nil,target);
		-- else
			-- SendChatMessage(L["TITAN_WG_TIME_UNTIL_WG"]..": "..TitanWG_formatTime(wgTime),channel,nil,target);
		end
	end
end

function TitanWG_getTradeChannel()
	local list = {GetChannelList()};
	for i=1,getn(list)/2 do
		if ( list[i*2] == L["TITAN_WG_TRADE"] ) then
			return list[i*2-1];
		end
	end
	return nil;
end

	--------------------------
	----- SYNC FUNCTIONS -----
	--------------------------
	
-- SYNC messages take the form: <cmd> <time?> <owner?>
-- Commands: REQUEST, UPDATE

function TitanWG_requestWGSync() 
	if ( TitanWG_State.state >= TitanWG_STATE_NOT_RECENTLY_IN_NORTHREND and
			( not TitanWG_isRequestPending ) and TitanWG_variablesLoaded ) then
		TitanWG_debug("Requesting sync...");
		-- Block other requests from going out. This should be set to 
		-- false just after the pool is processed
		TitanWG_isRequestPending = true;
		-- Initialize the time pool
		TitanWG_timePool = {};
		-- Send out the sync request to guild, raid/party, the 
		-- sync network, and friends
		if ( IsInGuild() ) then
			SendAddonMessage(TITAN_WG_ID, "REQUEST", "GUILD");
		end
		if ( GetNumRaidMembers() > 0 ) then
			SendAddonMessage(TITAN_WG_ID, "REQUEST", "RAID");
		elseif ( GetNumPartyMembers() > 0 ) then
			SendAddonMessage(TITAN_WG_ID, "REQUEST", "PARTY");
		end

		local friendsTable = {}; -- Make sure they're not in the sync net too		
		for i=1,GetNumFriends() do
			local name,_,_,_,connected,_,_ = GetFriendInfo(i);
			if ( name ~= nil and connected ) then
				friendsTable[name] = true;
			end
		end
		
		for player,lastTimeSeen in pairs(SyncNetData[TitanWG_realm]) do
			if ( UnitIsConnected(player) ) then
				-- Remove them from friendsTable
				friendsTable[player] = nil;
				-- If a player hasn't replied for a long time, removed them from the net
				if ( time() - lastTimeSeen < TitanWG_EXPIRED_SYNC_THRESHOLD ) then
					SendAddonMessage(TITAN_WG_ID, "REQUEST", "WHISPER", player);
				else
					TitanWG_debug("Expiring "..player.." from the sync net");
					SyncNetData[TitanWG_realm][player] = nil;
				end
			end
		end

		for player,_ in pairs(friendsTable) do
			SendAddonMessage(TITAN_WG_ID, "REQUEST", "WHISPER", player);
		end
		AceTimer.ScheduleTimer("TitanWG_processSyncPool", TitanWG_processSyncPool, TitanWG_REQUEST_TIME_S);
	end
end

function TitanWG_sendWGSync(channel, requester)
	if ( TitanWG_State.isInfoAccurate() ) then
		TitanWG_debug("Broadcasting sync to "..requester);
		local wgTime = TitanWG_State.time();
		local msg = "UPDATE "..wgTime;
		if ( TitanWG_owner == nil ) then
			msg = msg.." .";
		else
			msg = msg.." "..TitanWG_owner;
		end
		
		if ( ( channel == nil and IsInGuild() ) or channel == "GUILD" ) then
			SendAddonMessage(TITAN_WG_ID, msg, "GUILD");
		end
		if ( ( channel == nil and GetNumRaidMembers() > 0 ) or channel == "RAID" ) then
			SendAddonMessage(TITAN_WG_ID, msg, "RAID");
		end
		if ( ( channel == nil and GetNumPartyMembers() > 0 ) or channel == "PARTY" ) then
			SendAddonMessage(TITAN_WG_ID, msg, "PARTY");
		end
		if ( requester ~= nil ) then
			SendAddonMessage(TITAN_WG_ID, msg, "WHISPER", requester);
		end
	end
end

function TitanWG_processWGSync(replier, wgTime, wgOwner)
	if ( TitanWG_isRequestPending and ( not TitanWG_isProcessingPool ) ) then
		TitanWG_debug("Received sync from "..replier..": time="..wgTime..", owner="..wgOwner);
		TitanWG_timePool[replier] = wgTime;
		-- TODO: Should probably also process this in a way similar to the times
		if ( wgOwner ~= "." ) then
			TitanWG_owner = wgOwner;
		end
	else
		TitanWG_debug("Rejecting unrequested or late sync from "..replier);
	end
	-- Add them to our sync net
	-- TODO: Should probably make sure that this doesn't grow too large
	SyncNetData[TitanWG_realm][replier] = time();
end

function TitanWG_processSyncPool()
	TitanWG_debug("Processing sync pool...");
	TitanWG_isProcessingPool = true;
	local votesTable = {};
	local numReplies = 0;
	for player, timeSent in pairs(TitanWG_timePool) do
		numReplies = numReplies + 1;
		if ( votesTable[timeSent] ~= nil ) then
			votesTable[timeSent] = votesTable[timeSent] + 1;
		else
			votesTable[timeSent] = 1;
		end
	end
	TitanWG_debug("Processed "..numReplies.." replies.");
	if ( numReplies > 0 ) then
		-- Use majority voting to select the time; result undefined in
		-- the case of a tie
		local maxVotes = 0;
		local wgTime = 0;
		for timeSent,votes in pairs(votesTable) do
			if ( votes > maxVotes ) then
				maxVotes = votes;
				wgTime = tonumber(timeSent);
			end
		end
		TitanWG_debug("Winner recieved "..(100*maxVotes/numReplies).."% of the vote: "..maxVotes);
		-- TODO: Should timestamp things to compensate for latency in
		-- 	 replies and processing and such
		local adjTime = time() - TitanWG_REQUEST_TIME_S;
		local eps = abs(wgTime - TitanWG_State.time());
		local dt = adjTime - TimeOfLastTimes[TitanWG_realm];
		TimeOfLastTimes[TitanWG_realm] = adjTime; 
		LastTimesSeen[TitanWG_realm] = wgTime;
		TitanWG_learnEstimatedError(dt, eps);
	end
	TitanWG_isProcessingPool = false;
	TitanWG_isRequestPending = false;
end
